from typing import List, Literal

from PIL import Image as Img
from PIL import ImageFont
from PIL.Image import Image

import os
import json

import core
import images

def _get_resource_path(base:str, *levels:str, file_type:str=None) -> str:
    if len(levels) < 1:
        raise ValueError("Require a file at least")
    postfix = "" if not file_type else "." + file_type
    return os.path.realpath(os.path.join(base,*levels)) + postfix

def _get_image(base:str, *levels:str, file_type="png",size:tuple=None) -> Image:
    resource_path = _get_resource_path(base, *levels, file_type=file_type)
    with Img.open(fp=resource_path, mode="r") as i:
        i.load()
        if size: i = i.resize(size)
        return i
    
def _get_text(base:str, *levels:str, file_type:str="json") -> Image:
    resource_path = _get_resource_path(base, *levels,file_type=file_type)

    with open(resource_path, "r", encoding="utf-8") as f:
        text = f.read()
    return text
    
def get_ability(base:str, name:str) -> Image:
    """
    Get a ability
    """
    levels = ["abilities", "ability_{name}".format(name=name)]
    try:
        ability = _get_image(base, *levels)
    except FileNotFoundError as e:
        raise core.AppError("ability_not_found","Ability {name} not found".format(name=name)) from e
    else:
        return ability
    
def get_cost(base:str, cost:int, type: Literal["blood", "bone"]) -> Image:
    """
    Get the cost image for your card.
    For blood type, cost ranges from 0-4,
    and for bone, it ranges from 0-10 instead. 
    """
    if cost <= 0:
        return None
    if not type in ["blood", "bone"]:
        raise core.AppError("cost_type_bad","Invalid cost type {type}".format(type=type))

    COST_MIN = 1
    COST_MAX = 4 if type == "blood" else 10
    if not (COST_MIN <= cost <= COST_MAX):
        raise core.AppError("cost_bad","Invalid cost {cost} {type}".format(cost=cost, type=type))
    
    levels = ["costs","cost_{cost}{type}".format(cost=cost, type=type)]    
    return _get_image(base, *levels)

def get_card_base(base:str, name:str, type:Literal["front", "back"]) -> Image:
    """
    Get card front or card back
    """
    if not type in ["front", "back"]:
        raise core.AppError("card_base_bad_type","Invalid card base type {type}".format(type=type))
    
    levels = ["cards",type,"card_{type}_{name}".format(type=type, name=name)]
    try:
        card_base = _get_image(base, *levels)
    except FileNotFoundError as e:
        raise core.AppError("card_base_{type}_not_found".format(type=type),"Card Base {type} {name} not found".format(type=type, name=name)) from e
    
    return card_base

def get_decal(base:str, name:str) -> Image:
    levels = ["cards","decal","decal_{name}".format(name=name)]
    try:
        decal = _get_image(base, *levels)
    except FileNotFoundError as e:
        raise core.AppError("decal_not_found","Decal {name} not found".format(name=name)) from e
    return decal

def get_font(base:str, name:str) -> core.Font:
    levels_config = ["fonts", name, "font"]
    try:
        font_config = json.loads(_get_text(base, *levels_config, file_type="json"))
        font_file = font_config["file"]
    except FileNotFoundError as e:
        raise core.AppError("font_not_found","Font {name} not found".format(name=name)) from e
    except json.JSONDecodeError as e:
        raise core.AppError("font_config_not_found","Font {name} invalid config".format(name=name)) from e
    except KeyError as e:
        raise core.AppError("font_no_file_key","Font {name} config requires file key".format(name=name)) from e
    font_file_name, postfix = font_file.split(".",1)
    levels_file = ["fonts",name, font_file_name]
    file_path = _get_resource_path(base, *levels_file,file_type=postfix)
    try:
        _ = ImageFont.truetype(file_path)
    except OSError as e:
        raise core.AppError("font_not_found","Font {name} file {file} not found".format(name=name,file=font_file)) from e
    font = core.Font(file_path)
    return font

def get_card_data(base:str, name:str):
    try:
        t = _get_text(base, name, "card")
        data = json.loads(t)
        
    except FileNotFoundError as e:
        raise core.AppError("card_config_not_found","Card {name} config not found".format(name=name)) from e
    except json.JSONDecodeError as e:
        raise core.AppError("card_bad_config","Card {name} invalid config".format(name=name)) from e
    except KeyError as e:
        raise core.AppError("card_no_beast_key","Card {name} requires beast key".format(name=name)) from e
    try:
        beast = data["beast"]
        beast_img = _get_image(base, name, beast,file_type=None)
    except FileNotFoundError as e:
        raise core.AppError("card_beast_image_not_found","Card {name} texture {beast} not found".format(name=name, beast=beast)) from e
    return data, beast_img
    

def get_cards_data(arguments:str, error_queue:list=None) -> List[core.StaticCard]:
    if error_queue == None: error_queue = []
    card_folders = [folder for folder in os.listdir(arguments) if os.path.isdir(
        os.path.realpath(os.path.join(arguments, folder))
    )]
    cards = []
    for card_folder in card_folders:
        levels = [card_folder,"card"]
        try:
            t = get_card_data(arguments, name=card_folder)
        except core.AppError as e:
            error_queue.append("load_card_error:{}:{}".format(card_folder,e.id))
        else:
            cards.append((core.StaticCard(**t[0],id=card_folder),t[1]))
    return cards

def load_card(base, card:core.StaticCard, beast:Image, font_zh:core.Font, font_en:core.Font) -> core.Card:
    return core.Card(beast, 
        images.generate_text_image(card.name, font_zh, font_en), # name
        images.generate_text_image(str(card.damage),font_zh,font_en, 40, 60), # damage
        card.show_damage,
        images.generate_text_image(str(card.health),font_zh,font_en, 40, 60), # health
        card.show_health,
        get_cost(base, card.cost, card.cost_type),
        get_card_base(base, card.front, "front"),
        get_card_base(base, card.back, "back"),
        [get_ability(base,ability) for ability in card.abilities],
        None if not card.smoke else get_decal(base,"smoke_abilityhole")
        )